﻿using System;
using System.Collections;
using System.Diagnostics;
using System.IO;
using System.Threading;

namespace ForEachWatchedFileEnumerator
{
    public class WatchedDirectoryEnumerator : IEnumerable
    {
        private readonly string _watchFolderPath;
        private readonly string _fileSpec;
        private readonly bool _includeSubdirectories;
        private readonly int _timeOut;
        private readonly bool _processExistingFiles;

        private class FileWatcherEnumerator : IEnumerator
        {
            private readonly string _watchFolderPath;
            private readonly string _fileSpec;
            private readonly bool _includeSubdirectories;

            private readonly Stopwatch _stopwatch;
            private readonly int _timeOut;
            private string _current;
            private IEnumerator _directoryEnumerator;
            private readonly bool _processExistingFiles;

            public FileWatcherEnumerator(string watchFolderPath, string fileSpec, bool includeSubdirectories, int timeOut = 0, bool processExistingFiles = true)
            {
                _watchFolderPath = watchFolderPath;
                _fileSpec = fileSpec;
                _includeSubdirectories = includeSubdirectories;
                _processExistingFiles = processExistingFiles;
                _timeOut = timeOut * 1000;

                _stopwatch = new Stopwatch();
            }

            private int TimeRemaining
            {
                get
                {
                    return _timeOut - unchecked((int)_stopwatch.ElapsedMilliseconds);
                }
            }

            public bool MoveNext()
            {
                if (!_stopwatch.IsRunning)
                {
                    Reset();
                }

                if (TimeRemaining <= 0)
                {
                    _stopwatch.Stop();
                    return false;
                }

                if (_processExistingFiles)
                {
                    if (_directoryEnumerator == null)
                    {
                        _directoryEnumerator = Directory.GetFiles(_watchFolderPath, _fileSpec,
                            _includeSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)
                            .GetEnumerator();
                    }

                    if (_directoryEnumerator.MoveNext())
                    {
                        _current = (string)_directoryEnumerator.Current;
                        return true;
                    }
                }



                //If the timeout has not elapsed
                var fileSystemWatcher = new FileSystemWatcher
                {
                    Filter = _fileSpec,
                    IncludeSubdirectories = _includeSubdirectories,
                    Path = _watchFolderPath,
                    EnableRaisingEvents = true
                };

                var result = fileSystemWatcher.WaitForChanged(WatcherChangeTypes.Created, TimeRemaining);

                if (result.TimedOut || TimeRemaining <= 0) return false;

                //A created event has been detected on a file. Now wait until we can get an exclusive lock
                //i.e. so that the file has finished being written. Seems a bit hacky but doesn't look like
                //there is a better way. Due to the general hacky nature of file watching i guess.

                //This points to a bigger flaw in the code however. If another file is created between this one
                //and the time it takes us to set up another watch then we will never register that event.
                //Needs refactoring to using the event handler Created events in combination with Thread.Sleep
                //in order to ensure that no files are missed.

                //This will do for now however, as in our case the files will be picked up on next run of the SSIS job

                var createdFile = Path.Combine(_watchFolderPath, result.Name);

                Func<bool> isFileLocked = () =>
                {
                    try
                    {
                        using (var stream = File.Open(createdFile, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
                        {
                            return false;
                            return stream.Length > 0;
                        }
                    }
                    catch (IOException)
                    {
                        return true;
                    }
                };

                while (isFileLocked())
                {
                    Thread.Sleep(500);
                }

                _current = createdFile;

                return true;
            }

            public void Reset()
            {
                _stopwatch.Reset();
                _stopwatch.Start();
            }

            public object Current
            {
                get
                {
                    return _current;
                }
            }
        }

        public WatchedDirectoryEnumerator(string watchFolderPath, string fileSpec, bool includeSubdirectories,
            int timeOut = 0, bool processExistingFiles = false)
        {
            _watchFolderPath = watchFolderPath;
            _fileSpec = fileSpec;
            _includeSubdirectories = includeSubdirectories;
            _timeOut = timeOut;
            _processExistingFiles = processExistingFiles;
        }

        public IEnumerator GetEnumerator()
        {
            return new FileWatcherEnumerator(_watchFolderPath, _fileSpec, _includeSubdirectories, _timeOut, _processExistingFiles);
        }
    }
}
